﻿namespace EveKillboard.Core.Services
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Runtime.Caching;
    using System.Xml.XPath;
    using System.Collections;
    using EveKillboard.Core.Interfaces;

    public interface IEveApiServices : IDisposable
    {
        int? GetCharacterId(string name);
        CharacterInfos GetCharacterInfos(int id);
        CorporationInfos GetCorporationInfos(int id);
        IEnumerable<IFeedKillmail> GetCorporationKillmails(int keyId, string vCode);
        IEnumerable<IFeedKillmail> GetCharacterKillmails(int keyId, string vCode);
    }

    public class CharacterInfos
    {
        public int CharacterId { get; set; }
        public string CharacterName { get; set; }
        public int CorporationId { get; set; }
        public string CorporationName { get; set; }
        public int? AllianceId { get; set; }
        public string AllianceName { get; set; }
        public decimal SecurityStatus { get; set; }
        public IEnumerable<CharacterEmployment> Employments { get; set; }
    }

    public class CharacterEmployment
    {
        public int CorporationId { get; set; }
        public DateTime StartDate { get; set; }
    }

    public class CorporationInfos
    {
        public int CorporationId { get; set; }
        public string CorporationName { get; set; }
        public string Ticker { get; set; }
        public int? AllianceId { get; set; }
        public string AllianceName { get; set; }
        public string Description { get; set; }
        public int CeoCharacterId { get; set; }
        public string CeoCharacterName { get; set; }
        public float TaxRate { get; set; }
        public int MemberCount { get; set; }
        public string Headquarters { get; set; }
    }

    public class EveApiServicesProvider : IEveApiServices, ITraceSource
    {
        private const string DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
        private const string CACHE_XPATH = "/eveapi/cachedUntil";

        private const string CHARACTER_ID = "https://api.eveonline.com/eve/CharacterID.xml.aspx?names={0}";
        private const string CHARACTER_INFOS = "https://api.eveonline.com/eve/CharacterInfo.xml.aspx?characterID={0}";
        private const string CHARACTER_KILLS = "https://api.eveonline.com/char/KillLog.xml.aspx";
        private const string CORPORATION_SHEET = "https://api.eveonline.com/corp/CorporationSheet.xml.aspx?corporationID={0}";
        private const string CORPORATION_KILLS = "https://api.eveonline.com/corp/KillLog.xml.aspx";

        private MemoryCache _cache;


        static IEveApiServices _instance = null;
        public static IEveApiServices Instance
        {
            get { return _instance ?? (_instance = new EveApiServicesProvider()); }
        }
        
        public EveApiServicesProvider(TraceSource traceSource = null)
        {
            _cache = new MemoryCache("EveApiServices");
            Trace = traceSource ?? new TraceSource("EveApiServicesProvider");
        }

        public TraceSource Trace { get; set; }
        
        public int? GetCharacterId(string name)
        {
            if (string.IsNullOrEmpty(name)) return null;

            this.TraceEvent(TraceEventType.Information, "Requesting Character Name:\"{0}\" infos", name);

            var cacheKey = string.Format("CHAR_ID_{0}", name.Replace("'","_").Replace(" ","_"));
            if (_cache.Contains(cacheKey))
                try
                {
                    Trace.TraceEvent(TraceEventType.Verbose, "Retrieving data from cache");

                    return GetCachedData<int>(cacheKey);
                }
                catch (Exception exp)
                {
                    _cache.Remove(cacheKey);
                    Trace.TraceEvent(TraceEventType.Error, exp.Message);
                }

            int? result = null;
            try
            {
                var st = GetResponseStream(CreateRequest(Uri.EscapeUriString(string.Format(CHARACTER_ID, name))));
                if(st !=null)
                    using(st)
                    {
                        var nav = new XPathDocument(st).CreateNavigator();
                        Trace.TraceEvent(TraceEventType.Verbose, nav.OuterXml);
                        result = GetValue<int>(string.Format("/eveapi/result/rowset/row[@name=\"{0}\"]/@characterID", name), nav);

                        CacheResult(cacheKey, result.GetValueOrDefault(0), GetValue(CACHE_XPATH, nav));
                    }
            }
            catch(Exception exp)
            {
                this.TraceEvent(TraceEventType.Error, "CharacterId request with name \"{0}\" has thrown an exception of type \"{1}\". Details: {2}", name, exp.GetType(), exp.Message);
            }
            
            return result;
        }

        public CharacterInfos GetCharacterInfos(int id)
        {
            if (id == 0) return null;

            var cacheKey = string.Format("CHAR_INFOS_{0}", id);
            if (_cache.Contains(cacheKey))
                try
                {
                    return GetCachedData<CharacterInfos>(cacheKey);
                }
                catch (Exception exp)
                {
                    _cache.Remove(cacheKey);
                    Trace.TraceEvent(TraceEventType.Error, exp.Message);
                }

            this.TraceEvent(TraceEventType.Information, "Requesting Character Infos Id:\"{0}\" infos", id);

            CharacterInfos result = null;
            try
            {
                var st = GetResponseStream(CreateRequest(Uri.EscapeUriString(string.Format(CHARACTER_INFOS, id))));
                if (st != null)
                    using (st)
                    {
                        var nav = new XPathDocument(st).CreateNavigator();

                        this.TraceEvent(TraceEventType.Verbose, nav.OuterXml);

                        result = new CharacterInfos()
                                   {
                                       CharacterId = GetValue<int>("/eveapi/result/characterID", nav),
                                       CharacterName = GetValue("/eveapi/result/characterName", nav),
                                       CorporationId = GetValue<int>("/eveapi/result/corporationID", nav),
                                       CorporationName = GetValue("/eveapi/result/corporation", nav),
                                       AllianceId =  string.IsNullOrEmpty(GetValue("/eveapi/result/allianceID", nav)) ? (int?)null : int.Parse( GetValue("/eveapi/result/allianceID", nav) ),
                                       AllianceName = GetValue("/eveapi/result/alliance", nav),
                                       SecurityStatus = Math.Round(GetValue<decimal>("/eveapi/result/securityStatus", nav), 2),
                                       Employments = new List<CharacterEmployment>()
                                   };

                        var employments = nav.Select("/eveapi/result/rowset[@name=\"employmentHistory\"]/row");
                        if(employments!=null)
                            while (employments.MoveNext())
                            {
                                var current = employments.Current;
                                if(current ==null) continue;
                                
                                ((IList) result.Employments).Add(new CharacterEmployment()
                                                                     {
                                                                         CorporationId = int.Parse(current.GetAttribute("corporationID", "")),
                                                                         StartDate = DateTime.ParseExact(current.GetAttribute("startDate", ""), DATETIME_FORMAT, CultureInfo.InvariantCulture)
                                                                     });
                            }

                        CacheResult(cacheKey, result, GetValue(CACHE_XPATH, nav));
                    }
            }
            catch (Exception exp)
            {
                this.TraceEvent(TraceEventType.Error, "CharacterInfos request with id \"{0}\" has thrown an exception of type \"{1}\". Details: {2}", id, exp.GetType(), exp.Message);
            }

            return result;
        }

        public CorporationInfos GetCorporationInfos(int id)
        {
            if (id == 0) return null;

            this.TraceEvent(TraceEventType.Information, "Requesting Corporation Infos Id:\"{0}\" infos", id);

            var cacheKey = string.Format("CORP_INFOS_{0}", id);
            if (_cache.Contains(cacheKey))
                try
                {
                    return GetCachedData<CorporationInfos>(cacheKey);
                }
                catch (Exception exp)
                {
                    _cache.Remove(cacheKey);
                    this.TraceEvent(TraceEventType.Error, exp.Message);
                }

            CorporationInfos result = null;
            try
            {
                var st = GetResponseStream(CreateRequest(Uri.EscapeUriString(string.Format(CORPORATION_SHEET, id))));
                if (st != null)
                    using (st)
                    {
                        var nav = new XPathDocument(st).CreateNavigator();
                        result = new CorporationInfos()
                        {
                            CorporationId = GetValue<int>("/eveapi/result/corporationID", nav),
                            CorporationName = GetValue("/eveapi/result/corporationName", nav),
                            Ticker = GetValue("/eveapi/result/ticker", nav),
                            AllianceId = string.IsNullOrEmpty(GetValue("/eveapi/result/allianceID", nav)) ? (int?)null : int.Parse(GetValue("/eveapi/result/allianceID", nav)),
                            AllianceName = GetValue("/eveapi/result/allianceName", nav),
                            CeoCharacterId = GetValue<int>("/eveapi/result/ceoID", nav),
                            CeoCharacterName = GetValue("/eveapi/result/ceoName", nav),
                            TaxRate = GetValue<float>("/eveapi/result/taxRate", nav),
                            Headquarters = GetValue("/eveapi/result/stationName",nav),
                            MemberCount = GetValue<int>("/eveapi/result/memberCount", nav),
                            Description = GetValue("/eveapi/result/description", nav)
                        };

                        CacheResult(cacheKey, result, GetValue(CACHE_XPATH, nav));
                    }
            }
            catch (Exception exp)
            {
                this.TraceEvent(TraceEventType.Error, "CorporationInfos request with id \"{0}\" has thrown an exception of type \"{1}\". Details: {2}", id, exp.GetType(), exp.Message);
            }

            return result;
        }

        public IEnumerable<IFeedKillmail> GetCorporationKillmails(int keyId, string vCode)
        {
            if (keyId == 0 || string.IsNullOrEmpty(vCode)) return null;

            this.TraceEvent(TraceEventType.Information, "Requesting Corporation killog KeyId:\"{0}\" vCode:\"{1}\"", keyId, vCode);

            var cacheKey = string.Format("CORP_KILLS_{0}", keyId);
            if (_cache.Contains(cacheKey))
                try
                {
                    return GetCachedData<IEnumerable<IFeedKillmail>>(cacheKey);
                }
                catch (Exception exp)
                {
                    _cache.Remove(cacheKey);
                    this.TraceEvent(TraceEventType.Error, exp.Message);
                }

            IEnumerable<IFeedKillmail> kills = null;
            using (var provider = new CorporationKillmailFeedProvider(CORPORATION_KILLS, new Dictionary<int, string>() { { keyId, vCode } }))
                kills = provider.GetKillmails();
            
            if (kills.Any())
                CacheResult(cacheKey, kills, DateTime.UtcNow.AddHours(1));

            return kills;
        }

        public IEnumerable<IFeedKillmail> GetCharacterKillmails(int keyId, string vCode)
        {
            if (keyId == 0 || string.IsNullOrEmpty(vCode)) return null;

            this.TraceEvent(TraceEventType.Information, "Requesting Character killog KeyId:\"{0}\" vCode:\"{1}\"", keyId, vCode);

            var cacheKey = string.Format("CHAR_KILLS_{0}", keyId);
            if (_cache.Contains(cacheKey))
                try
                {
                    return GetCachedData<IEnumerable<IFeedKillmail>>(cacheKey);
                }
                catch (Exception exp)
                {
                    _cache.Remove(cacheKey);
                    this.TraceEvent(TraceEventType.Error, exp.Message);
                }

            IEnumerable<IFeedKillmail> kills = null;
            using (var provider = new CharacterKillmailFeedProvider(CHARACTER_KILLS, new Dictionary<int, string>() { { keyId, vCode } }))
                kills = provider.GetKillmails();

            if (kills.Any())
                CacheResult(cacheKey, kills, DateTime.UtcNow.AddHours(1));
            
            return kills;
        }

        private void CacheResult(string key, object data, string date)
        {
            if (string.IsNullOrEmpty(key) || _cache == null || data == null || string.IsNullOrEmpty(date)) return;

            DateTime dt;
            if (DateTime.TryParseExact(date, DATETIME_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt))
                CacheResult(key, data, dt);
        }

        private void CacheResult(string key, object data, DateTime date)
        {
            if (string.IsNullOrEmpty(key) || _cache == null || data == null ) return;
            try
            {
               _cache.Add(key, data, new DateTimeOffset(date));
            }
            catch (Exception exp)
            {
                this.TraceEvent(TraceEventType.Warning, "The attempt to cache data with key \"{2}\" has thrown an exception of type {0}. Details: {1}", exp.GetType(), exp.Message, key);
            }
        }

        public void Dispose()
        {
            if (_cache != null)
                _cache.Dispose();

            _cache = null;

            this.TraceEvent(TraceEventType.Verbose, "EveApiServicesProvider disposed");
        }

        private T GetCachedData<T>(string key)
        {
            this.TraceEvent(TraceEventType.Verbose, "Retrieving data from cache with key:{0}", key);
            return (T) _cache[key];
        }

        static string GetValue(string xpath, XPathNavigator navigator)
        {
            var node = navigator.SelectSingleNode(xpath);
            return node == null ? null : string.IsNullOrEmpty(node.Value) ? null  : node.Value;
        }

        static T GetValue<T>(string xpath, XPathNavigator navigator)
        {
            var node = navigator.SelectSingleNode(xpath);
            return node == null ? default(T) : string.IsNullOrEmpty(node.Value) ? default(T) : (T)node.ValueAs(typeof(T));
        }

        static Stream GetResponseStream(HttpWebRequest request, bool keepAlive = false)
        {
            if (request == null) return null;
            request.KeepAlive = keepAlive;
            var response = request.GetResponse() as HttpWebResponse;
            return response == null ? null : response.GetResponseStream();
        }

        static HttpWebRequest CreateRequest(string url)
        {
            return string.IsNullOrEmpty(url) ? null : Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) ? WebRequest.Create(url) as HttpWebRequest : null;
        }

        
    }
}